/**
 * Matter.js Server Implementation for Homebridge Plugin API
 *
 * This provides a Matter bridge that plugins can use to register
 * Matter accessories via the Homebridge API.
 */

import * as crypto from 'node:crypto'
import { EventEmitter } from 'node:events'
import * as fs from 'node:fs'
import { access, writeFile } from 'node:fs/promises'
import * as os from 'node:os'
import * as path from 'node:path'
import process from 'node:process'

import { Endpoint, Environment, ServerNode, StorageService, VendorId } from '@matter/main'
import { AggregatorEndpoint } from '@matter/main/endpoints'
import { ManualPairingCodeCodec, QrPairingCodeCodec } from '@matter/types/schema'
import * as fse from 'fs-extra'
import QRCode from 'qrcode-terminal'

import { Logger } from '../logger.js'
import getVersion from '../version.js'
import { MatterAccessoryCache } from './matterAccessoryCache.js'
import {
  HomebridgeColorControlServer,
  HomebridgeDoorLockServer,
  HomebridgeIdentifyServer,
  HomebridgeLevelControlServer,
  HomebridgeOnOffServer,
  HomebridgeThermostatServer,
  HomebridgeWindowCoveringServer,
  registerHandler,
  setAccessoriesMap,
} from './matterBehaviors.js'
import { errorHandler } from './matterErrorHandler.js'
import { networkMonitor } from './matterNetworkMonitor.js'
import { MatterServerConfig } from './matterSharedTypes.js'
import { MatterStorageManager } from './matterStorage.js'
import {
  clusters,
  deviceTypes,
  InternalMatterAccessory,
  MatterAccessory,
  MatterDeviceError,
  MatterFabricInfo,
  MatterServerEvents,
} from './matterTypes.js'
import { sanitizeUniqueId, truncateString, validatePort } from './matterValidation.js'

const log = Logger.withPrefix('Matter')

// Constants for Matter server configuration
const DEFAULT_MATTER_PORT = 5540
const DEFAULT_VENDOR_ID = 0xFFF1 // Test vendor ID from Matter spec
const DEFAULT_PRODUCT_ID = 0x8001 // Test product ID
const MAX_DEVICES_PER_BRIDGE = 1000 // Matter spec maximum devices per aggregator
const SERVER_READY_TIMEOUT_MS = 5000
const SERVER_READY_POLL_INTERVAL_MS = 100
const SERVER_INIT_DELAY_MS = 200
const MAX_PASSCODE_ATTEMPTS = 100
const FABRIC_MONITOR_INTERVAL_MS = 2000 // Check for fabric changes every 2 seconds

// Re-export for backward compatibility
export { MatterServerConfig } from './matterSharedTypes.js'

/**
 * Matter Server for Homebridge Plugin API
 * Allows plugins to register Matter accessories explicitly
 *
 * Extends EventEmitter to provide commissioning lifecycle events
 */
export class MatterServer extends EventEmitter {
  // Typed event emitter methods
  public declare on: <K extends keyof MatterServerEvents>(event: K, listener: MatterServerEvents[K]) => this
  public declare emit: <K extends keyof MatterServerEvents>(event: K, ...args: Parameters<MatterServerEvents[K]>) => boolean
  public declare removeListener: <K extends keyof MatterServerEvents>(event: K, listener: MatterServerEvents[K]) => this
  public declare removeAllListeners: (event?: keyof MatterServerEvents) => this

  private readonly config: MatterServerConfig
  private serverNode: ServerNode | null = null
  private aggregator: Endpoint<typeof AggregatorEndpoint> | null = null
  private accessories: Map<string, InternalMatterAccessory> = new Map()
  private isRunning = false
  private readonly MAX_DEVICES = MAX_DEVICES_PER_BRIDGE
  private shutdownHandler: (() => Promise<void>) | null = null

  // Internal commissioning values (generated, not user-configurable)
  private passcode: number = 0
  private discriminator: number = 0
  private readonly vendorId: number
  private readonly productId: number

  private commissioningInfo: {
    qrCode?: string
    manualPairingCode?: string
    qrCodeUrl?: string
  } = {}

  private serialNumber?: string
  private cleanupHandlers: Array<() => void | Promise<void>> = []
  private storageManager: MatterStorageManager | null = null
  private matterStoragePath?: string
  private accessoryCache: MatterAccessoryCache | null = null

  // Fabric monitoring
  private fabricMonitorInterval: ReturnType<typeof setInterval> | null = null
  private previousFabrics: Map<number, MatterFabricInfo> = new Map()

  constructor(config: MatterServerConfig) {
    super()

    // Store the validated config
    this.config = this.validateAndSanitizeConfig(config)

    // Enable debug logging if requested
    if (this.config.debugModeEnabled) {
      log.info('Matter debug mode enabled - verbose logging active')
    }

    // Initialize commissioning values (will be loaded from storage in start())
    this.vendorId = DEFAULT_VENDOR_ID
    this.productId = DEFAULT_PRODUCT_ID

    // Provide accessories map reference to behaviors for cache syncing
    setAccessoriesMap(this.accessories)
  }

  /**
   * Validate and sanitize Matter server configuration
   * Throws descriptive errors if configuration is invalid
   */
  private validateAndSanitizeConfig(config: MatterServerConfig): MatterServerConfig {
    const errors: string[] = []

    // Validate port
    const port = config.port || DEFAULT_MATTER_PORT
    const portValidation = validatePort(port, false)
    if (!portValidation.valid) {
      errors.push(`Invalid port: ${portValidation.error}`)
    }

    // Validate and sanitize uniqueId (REQUIRED)
    if (!config.uniqueId) {
      errors.push('uniqueId is required for Matter server configuration')
    }

    const rawUniqueId = config.uniqueId || ''
    const uniqueIdResult = sanitizeUniqueId(rawUniqueId)
    const uniqueId = uniqueIdResult.value

    if (uniqueId.length === 0) {
      errors.push('Invalid uniqueId: must be a non-empty string')
    }

    // Validate storagePath (if provided)
    let storagePath = config.storagePath
    if (storagePath !== undefined) {
      storagePath = path.resolve(storagePath) // Resolve to absolute path
    }

    // Validate and sanitize manufacturer
    let manufacturer = config.manufacturer
    if (manufacturer !== undefined) {
      manufacturer = truncateString(manufacturer, 32, 'Manufacturer name').value
    }

    // Validate and sanitize model
    let model = config.model
    if (model !== undefined) {
      model = truncateString(model, 32, 'Model name').value
    }

    // Validate firmwareRevision
    let firmwareRevision = config.firmwareRevision
    if (firmwareRevision !== undefined) {
      firmwareRevision = truncateString(firmwareRevision, 64, 'Firmware revision').value
    }

    // Validate serialNumber
    let serialNumber = config.serialNumber
    if (serialNumber !== undefined) {
      serialNumber = truncateString(serialNumber, 32, 'Serial number').value
    }

    // Validate debugModeEnabled
    const debugModeEnabled = config.debugModeEnabled || false

    // Throw if there are validation errors
    if (errors.length > 0) {
      throw new MatterDeviceError(
        `Matter configuration validation failed:\n${errors.map(e => `  - ${e}`).join('\n')}`,
      )
    }

    return {
      port,
      uniqueId,
      storagePath,
      manufacturer,
      model,
      firmwareRevision,
      serialNumber,
      debugModeEnabled,
    }
  }

  /**
   * Generate a secure random passcode
   * According to Matter spec, passcode must be:
   * - 8 digits (00000001 to 99999998)
   * - Not in the invalid list
   * - Not sequential or repeating patterns
   */
  private generateSecurePasscode(): number {
    let passcode: number
    const maxAttempts = MAX_PASSCODE_ATTEMPTS
    let attempts = 0

    const invalidPasscodes = [
      0,
      11111111,
      22222222,
      33333333,
      44444444,
      55555555,
      66666666,
      77777777,
      88888888,
      99999999,
      12345678,
      87654321,
    ]

    do {
      // Use cryptographically secure random number generation
      const randomBytes = crypto.randomBytes(4)
      const randomValue = randomBytes.readUInt32BE(0)
      // Generate a value between 1 and 99999998
      passcode = (randomValue % 99999998) + 1

      attempts++
      if (attempts > maxAttempts) {
        throw new Error('Failed to generate secure passcode after maximum attempts')
      }
    } while (
      invalidPasscodes.includes(passcode)
      || !this.isValidPasscode(passcode)
    )

    return passcode
  }

  /**
   * Validate a passcode according to Matter specifications
   */
  private isValidPasscode(passcode: number): boolean {
    // Must be between 1 and 99999998
    if (passcode < 1 || passcode > 99999998) {
      return false
    }

    // Convert to 8-digit string
    const passcodeStr = passcode.toString().padStart(8, '0')

    // Check for sequential patterns (12345678, 23456789, etc.)
    let isSequential = true
    for (let i = 1; i < passcodeStr.length; i++) {
      if (Number.parseInt(passcodeStr[i]) !== Number.parseInt(passcodeStr[i - 1]) + 1) {
        isSequential = false
        break
      }
    }
    if (isSequential) {
      return false
    }

    // Check for reverse sequential (87654321, 76543210, etc.)
    let isReverseSequential = true
    for (let i = 1; i < passcodeStr.length; i++) {
      if (Number.parseInt(passcodeStr[i]) !== Number.parseInt(passcodeStr[i - 1]) - 1) {
        isReverseSequential = false
        break
      }
    }
    if (isReverseSequential) {
      return false
    }

    // Check for too many repeating digits (more than 3 of same digit)
    const digitCounts = new Map<string, number>()
    for (const digit of passcodeStr) {
      digitCounts.set(digit, (digitCounts.get(digit) || 0) + 1)
      if (digitCounts.get(digit)! > 3) {
        return false
      }
    }

    return true
  }

  /**
   * Generate a random discriminator
   * According to Matter spec, discriminator must be:
   * - 12 bits (0-4095)
   * - Should be random for security
   */
  private generateRandomDiscriminator(): number {
    // Generate cryptographically secure random 12-bit discriminator (0-4095)
    const randomBytes = crypto.randomBytes(2)
    const discriminator = randomBytes.readUInt16BE(0) & 0x0FFF // Mask to 12 bits

    // Validate discriminator range
    if (discriminator < 0 || discriminator > 4095) {
      throw new Error(`Invalid discriminator generated: ${discriminator}`)
    }

    return discriminator
  }

  /**
   * Create ServerNode with automatic recovery from corrupted storage
   *
   * Matter.js can fail to start if fabric data is corrupted (common after
   * hard shutdowns or disk errors). This method implements automatic recovery by:
   *
   * 1. Attempting normal ServerNode creation
   * 2. If it fails with storage errors, identifying and removing corrupted files
   * 3. Retrying ServerNode creation with fresh storage
   *
   * This prevents the need for manual intervention while preserving data
   * safety by only removing storage on confirmed corruption errors.
   *
   * @param nodeOptions - Matter.js ServerNode configuration
   * @param sanitizedId - Filesystem-safe bridge identifier
   * @returns Initialized ServerNode instance
   * @throws Error if recovery fails or error is not storage-related
   */
  private async createServerNodeWithRecovery(nodeOptions: any, sanitizedId: string): Promise<ServerNode> {
    try {
      // First attempt to create ServerNode
      return await ServerNode.create(nodeOptions)
    } catch (error: any) {
      // Check if this is a storage corruption error
      const isStorageError = error?.message?.includes('Invalid public key encoding')
        || error?.message?.includes('FabricManager unavailable')
        || error?.message?.includes('key-input')
        || error?.cause?.message?.includes('Invalid public key encoding')

      if (!isStorageError) {
        // Not a storage error, rethrow
        throw error
      }

      // Storage is corrupted - clean up and retry
      log.warn('Detected corrupted Matter storage, attempting automatic recovery...')

      // The ServerNodeStore directory is inside our storage path with the same name as the bridge ID
      const environment = Environment.default
      const storageService = environment.get(StorageService)
      const storageLocation = storageService.location

      if (!storageLocation) {
        throw new Error('Storage location not set, cannot recover from corrupted storage')
      }

      const serverNodeStorePath = path.join(storageLocation, sanitizedId)
      const serverNodeStoreJsonFile = `${serverNodeStorePath}.json`

      try {
        let removedSomething = false

        // Delete the ServerNodeStore subdirectory
        if (fs.existsSync(serverNodeStorePath)) {
          log.info(`Removing corrupted ServerNodeStore directory: ${serverNodeStorePath}`)
          await fse.remove(serverNodeStorePath)
          removedSomething = true
        }

        // Delete the ServerNodeStore JSON file (contains fabric data)
        if (fs.existsSync(serverNodeStoreJsonFile)) {
          log.info(`Removing corrupted ServerNodeStore JSON file: ${serverNodeStoreJsonFile}`)
          await fse.remove(serverNodeStoreJsonFile)
          removedSomething = true
        }

        if (removedSomething) {
          log.info('Corrupted storage removed, retrying ServerNode creation...')
        } else {
          log.warn('No corrupted storage files found, corruption may be elsewhere')
        }

        // Retry ServerNode creation
        const serverNode = await ServerNode.create(nodeOptions)
        log.info('Successfully recovered from corrupted Matter storage')
        return serverNode
      } catch (retryError) {
        log.error('Failed to recover from corrupted storage:', retryError)
        log.error('Original error:', error)
        throw new Error(
          'Matter storage is corrupted and automatic recovery failed. '
          + `Please manually delete: ${serverNodeStorePath}`,
        )
      }
    }
  }

  /**
   * Start the Matter server
   */
  async start(): Promise<void> {
    if (this.isRunning) {
      log.warn('Matter server is already running')
      return
    }

    try {
      log.info('Starting Matter.js server...')

      // IMPORTANT: Storage must be configured BEFORE any Matter.js operations
      // This ensures persistent fabric data across restarts
      await this.setupStorage()

      // Load or generate commissioning credentials
      await this.loadOrGenerateCredentials()

      log.info(`Configuration: Port=${this.config.port}, Passcode=${this.passcode}, Discriminator=${this.discriminator}`)

      // Start network monitoring
      networkMonitor.startMonitoring()
      this.cleanupHandlers.push(() => networkMonitor.stopMonitoring())

      // Create commissioning options
      const commissioningOptions = {
        passcode: this.passcode,
        discriminator: this.discriminator,
      }

      log.info(`Using commissioning credentials: passcode=${this.passcode}, discriminator=${this.discriminator}`)

      // Use default name for the Matter bridge
      const bridgeName = 'Homebridge Matter Bridge'

      // Sanitize the uniqueId to ensure it's filesystem-safe
      // Replace any characters that could cause issues (colons, slashes, etc.)
      // Use only alphanumeric and hyphens, collapse multiple hyphens, trim leading/trailing hyphens
      const sanitizedId = this.config.uniqueId!.replace(/[^a-z0-9]/gi, '-').replace(/-+/g, '-').replace(/^-|-$/g, '')

      // Create node options with proper typing
      const nodeOptions = {
        id: sanitizedId,
        network: {
          port: this.config.port,
          ipv4: true, // Always enable IPv4 for Matter
        },
        commissioning: commissioningOptions,
        productDescription: {
          name: bridgeName,
          deviceType: AggregatorEndpoint.deviceType,
        },
        basicInformation: {
          nodeLabel: bridgeName.slice(0, 32), // Maximum 32 characters
          vendorId: VendorId(this.vendorId),
          vendorName: (this.config.manufacturer || 'Homebridge').slice(0, 32),
          productId: this.productId,
          productName: (this.config.model || 'Homebridge Matter Bridge').slice(0, 32),
          productLabel: bridgeName.slice(0, 64), // Maximum 64 characters
          serialNumber: this.serialNumber = this.config.serialNumber || this.generateSerialNumber(),
          hardwareVersion: 1,
          hardwareVersionString: os.release(),
          softwareVersion: 1,
          softwareVersionString: this.config.firmwareRevision || getVersion(),
          reachable: true,
        },
      }

      // Create server node with automatic recovery from corrupted storage
      this.serverNode = await this.createServerNodeWithRecovery(nodeOptions, sanitizedId)

      // Create aggregator endpoint for bridge pattern
      this.aggregator = new Endpoint(AggregatorEndpoint, {
        id: 'homebridge-aggregator',
      })

      // Add aggregator to server
      await this.serverNode.add(this.aggregator)

      // Generate and display commissioning information
      await this.generateCommissioningInfo()

      // Set up graceful shutdown handler
      this.shutdownHandler = async () => {
        log.info('Shutting down Matter server...')
        await this.stop()
      }

      // Register shutdown handlers
      process.on('SIGINT', this.shutdownHandler)
      process.on('SIGTERM', this.shutdownHandler)

      // Start the server in a non-blocking way
      this.serverNode.run().then(
        () => {
          log.info('Matter server stopped normally')
        },
        (error) => {
          log.error('Matter server stopped with error:', error)
          errorHandler.handleError(error, 'server-runtime')
        },
      )

      // Wait for server to be ready
      await this.waitForServerReady()

      // Load cached accessories (don't restore them yet - wait for plugins to re-register)
      if (this.accessoryCache) {
        await this.accessoryCache.load()
      }

      // Start fabric monitoring to emit commissioning events
      this.startFabricMonitoring()

      this.isRunning = true
      log.info(`Matter server started successfully on port ${this.config.port}`)
      log.info('Plugins can now register Matter accessories via the API')
    } catch (error) {
      log.error('Failed to start Matter server:', error)
      await this.cleanup()
      throw error
    }
  }

  /**
   * Set up and validate storage
   */
  private async setupStorage(): Promise<void> {
    if (!this.config.storagePath) {
      throw new Error('Storage path is required for Matter server')
    }

    // Resolve to absolute path and validate
    const storagePath = path.resolve(this.config.storagePath)
    const normalizedPath = path.normalize(storagePath)

    // Ensure path is within allowed directories
    const allowedBasePaths = [
      path.resolve(os.homedir(), '.homebridge'),
      path.resolve(process.cwd()),
      '/var/lib/homebridge', // Common system location
    ]

    const isAllowed = allowedBasePaths.some(basePath =>
      normalizedPath.startsWith(basePath),
    )

    if (!isAllowed || normalizedPath.includes('..')) {
      throw new Error(`Storage path not allowed: ${normalizedPath}. Must be within homebridge directories.`)
    }

    // Ensure the storage directory exists with proper permissions
    try {
      await fse.ensureDir(normalizedPath)
      await access(normalizedPath, fs.constants.R_OK | fs.constants.W_OK)
    } catch (error) {
      throw new Error(`Storage path not accessible: ${error}`)
    }

    // Create bridge-specific storage directory
    // Use only alphanumeric characters and hyphens for maximum compatibility
    const bridgeId = this.config.uniqueId?.replace(/[^a-z0-9]/gi, '-').replace(/-+/g, '-').replace(/^-|-$/g, '') || 'default'
    this.matterStoragePath = path.join(normalizedPath, bridgeId)
    await fse.ensureDir(this.matterStoragePath)

    // Create storage manager
    this.storageManager = new MatterStorageManager(this.matterStoragePath)

    // Create accessory cache
    this.accessoryCache = new MatterAccessoryCache(normalizedPath, bridgeId)

    // Configure environment to use our custom storage
    const environment = Environment.default
    const storageService = environment.get(StorageService)
    storageService.location = this.matterStoragePath

    // CRITICAL: Override storage factory with custom implementation
    // This ensures fabric data is properly persisted
    storageService.factory = (namespace: string) => {
      if (!this.storageManager) {
        throw new Error('Storage manager not initialized')
      }
      const storage = this.storageManager.getStorage(namespace)
      // Initialize asynchronously - Matter.js handles async storage properly
      storage.initialize().catch((error) => {
        log.error(`Failed to initialize storage namespace ${namespace}:`, error)
      })
      // Note: Cast to unknown first to satisfy TypeScript - our storage implements the required interface
      return storage as unknown as ReturnType<typeof storageService.factory>
    }

    // Add cleanup handler for storage
    this.cleanupHandlers.push(async () => {
      if (this.storageManager) {
        await this.storageManager.closeAll()
      }
    })

    log.info(`Matter storage initialized at: ${this.matterStoragePath}`)
  }

  /**
   * Load or generate commissioning credentials (passcode and discriminator)
   * These must be persistent across restarts to maintain the same QR code
   */
  private async loadOrGenerateCredentials(): Promise<void> {
    if (!this.storageManager) {
      throw new Error('Storage manager not initialized')
    }

    // Use 'credentials' namespace
    const storage = this.storageManager.getStorage('credentials')

    // CRITICAL: Initialize storage before reading to avoid race condition
    await storage.initialize()

    // Try to load existing credentials
    const storedPasscode = storage.get([], 'passcode') as number | undefined
    const storedDiscriminator = storage.get([], 'discriminator') as number | undefined

    if (storedPasscode && storedDiscriminator) {
      // Use stored credentials
      log.info('Loading existing commissioning credentials from storage')
      this.passcode = storedPasscode
      this.discriminator = storedDiscriminator
    } else {
      // Generate new credentials and store them
      log.info('Generating new commissioning credentials')
      this.passcode = this.generateSecurePasscode()
      this.discriminator = this.generateRandomDiscriminator()

      // Store for future use
      storage.set([], 'passcode', this.passcode)
      storage.set([], 'discriminator', this.discriminator)

      log.info('Commissioning credentials saved to storage')
    }
  }

  /**
   * Generate serial number for the bridge
   */
  private generateSerialNumber(): string {
    const timestamp = Date.now().toString(36).toUpperCase()
    const random = crypto.randomBytes(2).toString('hex').toUpperCase()
    return `HB-${timestamp}-${random}`
  }

  /**
   * Generate and display commissioning information
   */
  private async generateCommissioningInfo(): Promise<void> {
    const passcode = this.passcode.toString().padStart(8, '0')
    const discriminator = this.discriminator
    const vendorId = this.vendorId
    const productId = this.productId

    // Use Matter.js library to generate pairing codes properly
    const manualCode = ManualPairingCodeCodec.encode({
      discriminator,
      passcode: this.passcode,
    })

    // Format as XXXX-XXX-XXXX for display
    const manualPairingCode = `${manualCode.slice(0, 4)}-${manualCode.slice(4, 7)}-${manualCode.slice(7, 11)}`

    log.info(`Encoding QR code with: passcode=${this.passcode}, discriminator=${discriminator}, vendorId=${vendorId}, productId=${productId}`)

    const qrCodePayload = QrPairingCodeCodec.encode([{
      version: 0,
      vendorId,
      productId,
      flowType: 0, // Standard commissioning flow
      discoveryCapabilities: 4, // OnNetwork=4
      discriminator,
      passcode: this.passcode,
    }])

    log.info(`Generated QR code: ${qrCodePayload}`)
    log.info(`Generated manual code: ${manualPairingCode}`)

    // Store commissioning info
    this.commissioningInfo = {
      qrCode: qrCodePayload,
      manualPairingCode,
    }

    // Save commissioning info to disk for UI access
    try {
      if (!this.matterStoragePath) {
        throw new Error('Matter storage path not initialized')
      }
      const commissioningFilePath = path.join(this.matterStoragePath, 'commissioning.json')
      const commissioningData = {
        qrCode: qrCodePayload,
        manualPairingCode,
        serialNumber: this.serialNumber,
        passcode: this.passcode,
        discriminator: this.discriminator,
        commissioned: this.isCommissioned(),
      }
      await writeFile(commissioningFilePath, JSON.stringify(commissioningData, null, 2), 'utf-8')
      log.debug(`Saved commissioning info to ${commissioningFilePath}`)
    } catch (error: any) {
      log.warn(`Failed to save commissioning info to disk: ${error.message}`)
    }

    // Display commissioning information
    log.info(`\n${'='.repeat(60)}`)
    log.info('📱 MATTER COMMISSIONING INFORMATION')
    log.info('='.repeat(60))
    log.info(`Manual Pairing Code: ${manualPairingCode}`)
    log.info(`Passcode: ${passcode}`)
    log.info(`Discriminator: ${discriminator}`)
    log.info('\nQR Code for commissioning:')

    // Generate and display QR code in terminal
    QRCode.generate(qrCodePayload, { small: true }, (qrcode) => {
      // eslint-disable-next-line no-console
      console.log(qrcode)
    })

    log.info(`${'='.repeat(60)}\n`)
  }

  /**
   * Wait for the server to be ready
   */
  private async waitForServerReady(maxWaitTime = SERVER_READY_TIMEOUT_MS): Promise<void> {
    const startTime = Date.now()

    while (!this.serverNode || !this.aggregator) {
      if (Date.now() - startTime > maxWaitTime) {
        throw new Error('Server failed to become ready within timeout')
      }
      await new Promise(resolve => setTimeout(resolve, SERVER_READY_POLL_INTERVAL_MS))
    }

    // Additional small delay to ensure everything is initialized
    await new Promise(resolve => setTimeout(resolve, SERVER_INIT_DELAY_MS))
  }

  /**
   * Start monitoring fabric changes to emit commissioning events
   */
  private startFabricMonitoring(): void {
    // Stop any existing monitor
    this.stopFabricMonitoring()

    // Initialize with current fabrics
    const initialFabrics = this.getFabricInfo()
    for (const fabric of initialFabrics) {
      this.previousFabrics.set(fabric.fabricIndex, fabric)
    }

    log.debug('Starting fabric monitoring for commissioning events')

    // Set up periodic monitoring
    this.fabricMonitorInterval = setInterval(() => {
      this.checkFabricChanges()
    }, FABRIC_MONITOR_INTERVAL_MS)

    // Add to clean up handlers
    this.cleanupHandlers.push(() => this.stopFabricMonitoring())
  }

  /**
   * Stop fabric monitoring
   */
  private stopFabricMonitoring(): void {
    if (this.fabricMonitorInterval) {
      clearInterval(this.fabricMonitorInterval)
      this.fabricMonitorInterval = null
      log.debug('Stopped fabric monitoring')
    }
  }

  /**
   * Check for fabric changes and emit appropriate events
   */
  private checkFabricChanges(): void {
    try {
      const currentFabrics = this.getFabricInfo()
      const currentFabricMap = new Map<number, MatterFabricInfo>()

      // Build map of current fabrics
      for (const fabric of currentFabrics) {
        currentFabricMap.set(fabric.fabricIndex, fabric)
      }

      const previousCount = this.previousFabrics.size
      const currentCount = currentFabricMap.size

      // Check for added fabrics
      for (const [fabricIndex, fabric] of currentFabricMap) {
        if (!this.previousFabrics.has(fabricIndex)) {
          log.info(`Fabric added: ${fabric.fabricId} (index: ${fabricIndex})`)
          this.emit('fabric-added', fabric)

          // If this is the first fabric, emit 'commissioned' event
          if (previousCount === 0 && currentCount === 1) {
            log.info('Bridge commissioned for the first time')
            this.emit('commissioned', fabric)
          }
        }
      }

      // Check for removed fabrics
      for (const [fabricIndex, fabric] of this.previousFabrics) {
        if (!currentFabricMap.has(fabricIndex)) {
          log.info(`Fabric removed: ${fabric.fabricId} (index: ${fabricIndex})`)
          this.emit('fabric-removed', fabric)

          // If this was the last fabric, emit 'decommissioned' event
          if (previousCount === 1 && currentCount === 0) {
            log.info('Bridge decommissioned (last fabric removed)')
            this.emit('decommissioned')
          }
        }
      }

      // Emit general commissioning-changed event if count changed
      if (previousCount !== currentCount) {
        const commissioned = currentCount > 0
        log.debug(`Commissioning state changed: commissioned=${commissioned}, fabricCount=${currentCount}`)
        this.emit('commissioning-changed', commissioned, currentCount)

        // Update commissioning info file
        this.updateCommissioningFile().catch((error) => {
          log.warn('Failed to update commissioning file:', error)
        })
      }

      // Update previous fabrics map
      this.previousFabrics = currentFabricMap
    } catch (error) {
      log.error('Error checking fabric changes:', error)
    }
  }

  /**
   * Update commissioning info file when commissioning state changes
   */
  private async updateCommissioningFile(): Promise<void> {
    try {
      if (!this.matterStoragePath) {
        return
      }

      const commissioningFilePath = path.join(this.matterStoragePath, 'commissioning.json')
      const commissioningData = {
        qrCode: this.commissioningInfo.qrCode,
        manualPairingCode: this.commissioningInfo.manualPairingCode,
        serialNumber: this.serialNumber,
        passcode: this.passcode,
        discriminator: this.discriminator,
        commissioned: this.isCommissioned(),
        fabricCount: this.getCommissionedFabricCount(),
        fabrics: this.getFabricInfo(),
      }
      await writeFile(commissioningFilePath, JSON.stringify(commissioningData, null, 2), 'utf-8')
      log.debug('Updated commissioning info file')
    } catch (error: any) {
      log.debug(`Failed to update commissioning info file: ${error.message}`)
    }
  }

  /**
   * Register a Matter accessory (Plugin API)
   */
  async registerAccessory(accessory: MatterAccessory): Promise<void> {
    if (!this.serverNode || !this.aggregator) {
      throw new MatterDeviceError('Matter server not started')
    }

    // Validate required fields with helpful error messages
    if (!accessory.deviceType) {
      throw new MatterDeviceError(
        `Matter accessory "${accessory.displayName || 'unknown'}" is missing required field 'deviceType'. `
        + 'Example: deviceType: api.matterDeviceTypes.OnOffLight\n'
        + 'Available device types: OnOffLight, DimmableLight, TemperatureSensor, etc.\n'
        + 'See the Matter types documentation for the full list.',
      )
    }

    if (!accessory.uuid) {
      throw new MatterDeviceError(
        'Matter accessory is missing required field \'uuid\'.\n'
        + 'Generate a unique UUID for your accessory:\n'
        + '  const uuid = api.hap.uuid.generate(\'my-unique-id\')',
      )
    }

    if (!accessory.displayName) {
      throw new MatterDeviceError(
        `Matter accessory (${accessory.uuid}) is missing required field 'displayName'.\n`
        + 'Example: displayName: \'Living Room Light\'',
      )
    }

    if (!accessory.serialNumber) {
      throw new MatterDeviceError(
        `Matter accessory "${accessory.displayName}" is missing required field 'serialNumber'.\n`
        + 'Example: serialNumber: \'ABC123\' or serialNumber: accessory.UUID',
      )
    }

    if (!accessory.manufacturer) {
      throw new MatterDeviceError(
        `Matter accessory "${accessory.displayName}" is missing required field 'manufacturer'.\n`
        + 'Example: manufacturer: \'Homebridge\' or manufacturer: \'My Plugin Name\'',
      )
    }

    if (!accessory.model) {
      throw new MatterDeviceError(
        `Matter accessory "${accessory.displayName}" is missing required field 'model'.\n`
        + 'Example: model: \'v1.0\' or model: \'Smart Light\'',
      )
    }

    if (!accessory.clusters || typeof accessory.clusters !== 'object') {
      throw new MatterDeviceError(
        `Matter accessory "${accessory.displayName}" is missing or has invalid 'clusters' field.\n`
        + 'Clusters define the functionality of your device. Example:\n'
        + '  clusters: {\n'
        + '    onOff: { onOff: false },\n'
        + '    levelControl: { currentLevel: 0, minLevel: 0, maxLevel: 254 }\n'
        + '  }',
      )
    }

    // Check if already registered (during this session)
    if (this.accessories.has(accessory.uuid)) {
      const existing = this.accessories.get(accessory.uuid)
      throw new MatterDeviceError(
        `Matter accessory with UUID "${accessory.uuid}" is already registered.\n`
        + `Existing accessory: "${existing?.displayName}"\n`
        + `New accessory: "${accessory.displayName}"\n`
        + 'Each accessory must have a unique UUID. Use api.hap.uuid.generate() with a unique string.',
      )
    }

    // Check if there's a cached version - merge cached cluster states with new registration
    if (this.accessoryCache && this.accessoryCache.hasCached(accessory.uuid)) {
      const cached = this.accessoryCache.getCached(accessory.uuid)
      if (cached && cached.clusters) {
        // Merge cached cluster states with new ones (prefer cached state to persist values across restarts)
        for (const [clusterName, cachedAttrs] of Object.entries(cached.clusters)) {
          if (!accessory.clusters[clusterName]) {
            // Cluster exists in cache but not in new registration - preserve it
            accessory.clusters[clusterName] = cachedAttrs
          } else {
            // Cluster exists in both - merge (prefer cached state over initial values)
            accessory.clusters[clusterName] = {
              ...accessory.clusters[clusterName],
              ...cachedAttrs,
            }
          }
        }

        // Restore context if available
        if (cached.context && !accessory.context) {
          accessory.context = cached.context
        }

        log.info(`Restored cached state for Matter accessory: ${accessory.displayName}`)
      }
    }

    // Check device limit
    if (this.accessories.size >= this.MAX_DEVICES) {
      throw new MatterDeviceError(
        `Cannot register Matter accessory "${accessory.displayName}": `
        + `Maximum device limit reached (${this.MAX_DEVICES} devices).\n`
        + `Current registered devices: ${this.accessories.size}`,
      )
    }

    try {
      // Modify device type with custom behaviors if handlers are defined
      let deviceType = accessory.deviceType

      if (accessory.handlers) {
        // Map cluster names to custom behavior classes
        // Only clusters with user-triggered commands need custom behaviors
        const behaviorMap: Record<string, any> = {
          // Core controls
          onOff: HomebridgeOnOffServer,
          levelControl: HomebridgeLevelControlServer,
          colorControl: HomebridgeColorControlServer,

          // Coverings & locks
          windowCovering: HomebridgeWindowCoveringServer,
          doorLock: HomebridgeDoorLockServer,

          // Climate control
          thermostat: HomebridgeThermostatServer,

          // Identification
          identify: HomebridgeIdentifyServer,
        }

        // Build array of custom behaviors to apply based on what handlers are defined
        const customBehaviors: any[] = []

        for (const clusterName of Object.keys(accessory.handlers)) {
          const behaviorClass = behaviorMap[clusterName]
          if (behaviorClass) {
            customBehaviors.push(behaviorClass)
            log.info(`Will use ${behaviorClass.name} for ${accessory.displayName}`)
          } else {
            log.warn(`No custom behavior class available for cluster '${clusterName}' - handlers will be registered but may not be called`)
          }
        }

        if (customBehaviors.length > 0) {
          // Cast to any to bypass TypeScript limitations
          deviceType = (deviceType as any).with(...customBehaviors)
          log.info(`Applied ${customBehaviors.length} custom behavior(s) to device type`)
        }
      }

      // Create endpoint with the modified device type
      const endpoint = new Endpoint(deviceType, {
        id: accessory.uuid,
      })

      // Add to aggregator FIRST (required before we can configure it)
      await this.aggregator.add(endpoint)

      if (this.config.debugModeEnabled) {
        log.debug(`Added endpoint for ${accessory.displayName} to aggregator`)
      }

      // NOW configure the endpoint
      await this.configureEndpoint(endpoint, accessory)

      // Store accessory
      const internalAccessory: InternalMatterAccessory = {
        ...accessory,
        endpoint,
        registered: true,
      }
      this.accessories.set(accessory.uuid, internalAccessory)

      log.info(`Registered Matter accessory: ${accessory.displayName} (${accessory.uuid})`)

      if (this.config.debugModeEnabled) {
        log.debug(`Total registered accessories: ${this.accessories.size}/${this.MAX_DEVICES}`)
      }

      // Emit accessory-registered event
      this.emit('accessory-registered', accessory)

      // Save to cache asynchronously (don't block registration)
      if (this.accessoryCache) {
        this.accessoryCache.save(this.accessories).catch((error) => {
          log.warn('Failed to save accessory cache:', error)
        })
      }
    } catch (error) {
      log.error(`Failed to register Matter accessory ${accessory.displayName}:`, error)
      throw new MatterDeviceError(`Failed to register accessory: ${error}`)
    }
  }

  /**
   * Unregister a Matter accessory (Plugin API)
   */
  async unregisterAccessory(uuid: string): Promise<void> {
    const accessory = this.accessories.get(uuid)
    if (!accessory) {
      log.debug(`Accessory ${uuid} not found, ignoring unregister request`)
      return
    }

    try {
      if (accessory.endpoint && this.aggregator) {
        await accessory.endpoint.close()
        log.debug(`Removed endpoint for ${accessory.displayName}`)
      }

      this.accessories.delete(uuid)
      log.info(`Unregistered Matter accessory: ${accessory.displayName} (${uuid})`)

      // Emit accessory-unregistered event
      this.emit('accessory-unregistered', uuid)

      // Update cache (remove the accessory)
      if (this.accessoryCache) {
        this.accessoryCache.removeCached(uuid)
        this.accessoryCache.save(this.accessories).catch((error) => {
          log.warn('Failed to save accessory cache:', error)
        })
      }
    } catch (error) {
      log.error(`Failed to unregister Matter accessory ${uuid}:`, error)
      throw new MatterDeviceError(`Failed to unregister accessory: ${error}`)
    }
  }

  /**
   * Update a Matter accessory's state (Plugin API)
   *
   * This method can be called from anywhere, including from within handlers.
   * State updates are automatically deferred to avoid transaction conflicts.
   */
  async updateAccessoryState(uuid: string, cluster: string, attributes: Record<string, unknown>): Promise<void> {
    const accessory = this.accessories.get(uuid)
    if (!accessory || !accessory.endpoint) {
      throw new MatterDeviceError(`Accessory ${uuid} not found or not registered`)
    }

    // Defer the update to avoid "read-only transaction" errors when called from handlers
    // Matter.js uses transactions, and we need to wait until the transaction fully completes
    // Use setTimeout with a delay to ensure we're completely outside the transaction
    return new Promise((resolve, reject) => {
      setTimeout(async () => {
        try {
          // Use endpoint.set() method which is the proper way to update state
          // This handles transactions correctly
          const endpoint = accessory.endpoint as any

          // Construct the update object
          const updateObject = { [cluster]: attributes }

          // Use endpoint.set() which properly handles state updates
          await endpoint.set(updateObject)

          // CRITICAL: Also update the cached clusters object so state persists across restarts
          // Merge the new attributes into the existing cluster state
          if (!accessory.clusters[cluster]) {
            accessory.clusters[cluster] = {}
          }
          accessory.clusters[cluster] = {
            ...accessory.clusters[cluster],
            ...attributes,
          }

          log.debug(`Updated ${cluster} state for ${accessory.displayName}:`, attributes)
          resolve()
        } catch (error) {
          log.error(`Failed to update state for accessory ${uuid}:`, error)
          reject(new MatterDeviceError(`Failed to update accessory state: ${error}`))
        }
      }, 50) // 50ms delay to ensure we're completely outside the transaction
    })
  }

  /**
   * Get a Matter accessory's current state (Plugin API)
   *
   * Returns the current cluster attribute values that are exposed to Matter controllers.
   * This is useful for:
   * - Reading state after plugin restart (when local variables are lost)
   * - Verifying current state before making changes
   * - Multiple parts of code that need to read state
   * - Debugging and logging
   *
   * @param uuid - The UUID of the accessory
   * @param cluster - The cluster name (e.g., 'onOff', 'levelControl')
   * @returns Current cluster attribute values, or undefined if cluster not found
   */
  getAccessoryState(uuid: string, cluster: string): Record<string, unknown> | undefined {
    const accessory = this.accessories.get(uuid)
    if (!accessory || !accessory.endpoint) {
      log.debug(`Accessory ${uuid} not found or not registered`)
      return undefined
    }

    try {
      const endpoint = accessory.endpoint as any

      if (!endpoint.state) {
        log.debug(`endpoint.state is undefined for ${accessory.displayName}`)
        return undefined
      }

      if (!endpoint.state[cluster]) {
        const availableClusters = Object.keys(endpoint.state || {})
        log.debug(`Cluster '${cluster}' not found on ${accessory.displayName}. Available: ${availableClusters.join(', ')}`)
        return undefined
      }

      const clusterState = endpoint.state[cluster]

      // Build result object by reading each property directly
      const result: Record<string, unknown> = {}

      // Get list of properties to read - use both approaches for maximum compatibility
      const allKeys = new Set([
        ...Object.keys(clusterState),
        ...Object.getOwnPropertyNames(clusterState),
      ])

      for (const key of allKeys) {
        try {
          // Skip internal properties, methods, and symbols
          if (key.startsWith('_') || key.startsWith('$')) {
            continue
          }

          // Try to read the value directly
          const value = clusterState[key]

          // Skip functions and undefined values
          if (typeof value === 'function' || value === undefined) {
            continue
          }

          result[key] = value
        } catch (propError) {
          log.debug(`Could not read property ${key} from ${cluster}:`, propError)
        }
      }

      if (Object.keys(result).length === 0) {
        log.debug(`Cluster ${cluster} found but no readable properties on accessory ${accessory.displayName}`)
        return undefined
      }

      return result
    } catch (error) {
      log.error(`Failed to get state for accessory ${uuid}:`, error)
      return undefined
    }
  }

  /**
   * Get all registered accessories (Plugin API)
   */
  getAccessories(): MatterAccessory[] {
    return Array.from(this.accessories.values()).map((acc) => {
      // Return copy without internal fields
      // eslint-disable-next-line unused-imports/no-unused-vars
      const { endpoint, registered, ...publicAccessory } = acc
      return publicAccessory
    })
  }

  /**
   * Get a specific accessory by UUID (Plugin API)
   */
  getAccessory(uuid: string): MatterAccessory | undefined {
    const accessory = this.accessories.get(uuid)
    if (!accessory) {
      return undefined
    }

    // Return copy without internal fields
    // eslint-disable-next-line unused-imports/no-unused-vars
    const { endpoint, registered, ...publicAccessory } = accessory
    return publicAccessory
  }

  /**
   * Configure a Matter endpoint after it's been added to the aggregator
   */
  private async configureEndpoint(endpoint: Endpoint, accessory: MatterAccessory): Promise<void> {
    // Note: bridgedDeviceBasicInformation is not available in Matter.js v0.15.4
    // The BridgedDeviceBasicInformation cluster is automatically added to bridged devices
    // but cannot be configured via endpoint.set() in this version
    // Leaving this commented out to avoid error logs
    // try {
    //   await endpoint.set({
    //     bridgedDeviceBasicInformation: {
    //       nodeLabel: accessory.displayName.slice(0, 32),
    //       vendorName: accessory.manufacturer.slice(0, 32),
    //       vendorId: VendorId(this.vendorId),
    //       productName: accessory.model.slice(0, 32),
    //       productLabel: accessory.displayName.slice(0, 64),
    //       serialNumber: accessory.serialNumber,
    //       reachable: true,
    //       ...(accessory.hardwareRevision && { hardwareVersionString: accessory.hardwareRevision }),
    //       ...(accessory.softwareVersion && { softwareVersionString: accessory.softwareVersion }),
    //     },
    //   } as any)
    // } catch (error) {
    //   log.debug(`Could not set bridgedDeviceBasicInformation for ${accessory.displayName}: ${error}`)
    // }

    // Set up cluster states
    for (const [clusterName, attributes] of Object.entries(accessory.clusters)) {
      // Cast to any temporarily to work around Matter.js type limitations
      await endpoint.set({ [clusterName]: attributes } as any)
    }

    // Set up command handlers if provided
    if (accessory.handlers) {
      log.info(`Setting up handlers for accessory ${accessory.uuid}`)

      // Register handlers with the custom behavior classes
      for (const [clusterName, handlers] of Object.entries(accessory.handlers)) {
        log.info(`  Processing cluster: ${clusterName}`)

        for (const [commandName, handler] of Object.entries(handlers)) {
          registerHandler(accessory.uuid, clusterName, commandName, handler)
        }
      }
    }
  }

  /**
   * Stop the Matter server
   */
  async stop(): Promise<void> {
    if (!this.isRunning) {
      log.debug('Matter server is not running')
      return
    }

    this.isRunning = false

    // Stop monitoring
    this.stopFabricMonitoring()
    networkMonitor.stopMonitoring()

    try {
      // Save accessory cache before shutting down (BEFORE clearing accessories!)
      if (this.accessoryCache && this.accessories.size > 0) {
        await this.accessoryCache.save(this.accessories)
        log.debug('Saved accessory cache before shutdown')
      }

      // Clean up all accessories
      for (const accessory of this.accessories.values()) {
        try {
          if (accessory.endpoint) {
            await accessory.endpoint.close()
          }
        } catch (error) {
          log.error('Failed to clean up accessory:', error)
        }
      }
      this.accessories.clear()

      // Stop server
      if (this.serverNode) {
        await this.serverNode.close()
      }

      await this.cleanup()
      log.info('Matter server stopped')
    } catch (error) {
      log.error('Error stopping Matter server:', error)
      await errorHandler.handleError(error as Error, 'server-stop')
      throw error
    } finally {
      this.isRunning = false
    }
  }

  /**
   * Cleanup resources
   */
  private async cleanup(): Promise<void> {
    // Remove signal handlers
    if (this.shutdownHandler) {
      process.off('SIGINT', this.shutdownHandler)
      process.off('SIGTERM', this.shutdownHandler)
      this.shutdownHandler = null
    }

    // Run all cleanup handlers
    for (const handler of this.cleanupHandlers) {
      try {
        await handler()
      } catch (error) {
        log.debug('Error during cleanup handler:', error)
      }
    }
    this.cleanupHandlers = []

    // Clear references
    this.serverNode = null
    this.aggregator = null
    this.isRunning = false
    this.commissioningInfo = {}
  }

  /**
   * Get fabric information for commissioned controllers
   */
  getFabricInfo(): Array<{
    fabricIndex: number
    fabricId: string
    nodeId: string
    rootVendorId: number
    label?: string
  }> {
    try {
      if (!this.serverNode) {
        return []
      }

      // Access fabric information from server state
      // Note: Matter.js ServerNode types don't expose state properly, needs runtime check
      interface ServerNodeWithState {
        state?: {
          commissioning?: {
            fabrics?: Array<{
              fabricIndex: number
              fabricId?: { toString: () => string }
              nodeId?: { toString: () => string }
              rootVendorId?: number
              label?: string
            }>
          }
        }
      }
      const serverState = this.serverNode as unknown as ServerNodeWithState
      const fabrics = serverState?.state?.commissioning?.fabrics

      // Ensure fabrics is an array before mapping
      if (!Array.isArray(fabrics)) {
        return []
      }

      return fabrics.map(fabric => ({
        fabricIndex: fabric.fabricIndex,
        fabricId: fabric.fabricId?.toString() || '',
        nodeId: fabric.nodeId?.toString() || '',
        rootVendorId: fabric.rootVendorId || 0,
        label: fabric.label,
      }))
    } catch (error) {
      log.error('Failed to get fabric info:', error)
      return []
    }
  }

  /**
   * Check if the server is commissioned
   */
  isCommissioned(): boolean {
    const fabrics = this.getFabricInfo()
    return fabrics.length > 0
  }

  /**
   * Get the number of commissioned fabrics
   */
  getCommissionedFabricCount(): number {
    return this.getFabricInfo().length
  }

  /**
   * Get server status information
   */
  getServerInfo(): {
    running: boolean
    port: number
    deviceCount: number
    commissioned: boolean
    fabricCount: number
    serialNumber?: string
  } {
    return {
      running: this.isRunning,
      port: this.config.port || 5540,
      deviceCount: this.accessories.size,
      commissioned: this.isCommissioned(),
      fabricCount: this.getCommissionedFabricCount(),
      serialNumber: this.serialNumber,
    }
  }

  /**
   * Get commissioning information
   */
  getCommissioningInfo(): {
    qrCode?: string
    manualPairingCode?: string
    serialNumber?: string
    passcode?: number
    discriminator?: number
    commissioned: boolean
  } {
    return {
      ...this.commissioningInfo,
      serialNumber: this.serialNumber,
      passcode: this.passcode,
      discriminator: this.discriminator,
      commissioned: this.isCommissioned(),
    }
  }

  /**
   * Get storage statistics
   */
  getStorageStats(): Array<{ entries: number, namespace: string, path: string }> | null {
    if (!this.storageManager) {
      return null
    }
    return this.storageManager.getAllStats()
  }

  /**
   * Check if server is running
   */
  isServerRunning(): boolean {
    return this.isRunning
  }

  /**
   * Get Matter device types available for plugin use
   */
  getDeviceTypes() {
    return deviceTypes
  }

  /**
   * Get Matter clusters available for plugin use
   */
  getClusters() {
    return clusters
  }

  /**
   * Remove a specific fabric (controller) from the bridge
   * This decommissions a single controller while leaving others intact
   *
   * @param fabricIndex - The fabric index to remove
   * @returns Promise that resolves when the fabric is removed
   */
  async removeFabric(fabricIndex: number): Promise<void> {
    if (!this.serverNode) {
      throw new MatterDeviceError('Matter server not started')
    }

    try {
      log.info(`Removing fabric ${fabricIndex}...`)

      // Access the FabricManager from the server node
      // Note: Matter.js ServerNode types don't expose this properly, needs runtime check
      interface ServerNodeWithFabrics {
        state?: {
          commissioning?: {
            removeFabric?: (fabricIndex: number) => Promise<void>
          }
        }
      }
      const serverState = this.serverNode as unknown as ServerNodeWithFabrics
      const removeFabric = serverState?.state?.commissioning?.removeFabric

      if (typeof removeFabric !== 'function') {
        throw new MatterDeviceError('Fabric removal not supported by Matter.js version')
      }

      // Remove the fabric
      await removeFabric(fabricIndex)

      log.info(`Fabric ${fabricIndex} removed successfully`)

      // The fabric monitoring will detect this change and emit the appropriate events
    } catch (error: any) {
      log.error(`Failed to remove fabric ${fabricIndex}:`, error)
      throw new MatterDeviceError(`Failed to remove fabric: ${error.message}`, error)
    }
  }

  /**
   * Check if a specific fabric exists
   */
  hasFabric(fabricIndex: number): boolean {
    const fabrics = this.getFabricInfo()
    return fabrics.some(f => f.fabricIndex === fabricIndex)
  }
}
